Skip to content

feat: performance attribution & benchmark-relative return reporting - #331

Merged
robertocarlous merged 2 commits into
Neurowealth:mainfrom
pope-h:feat/issue-320-performance-attribution
Aug 19, 2026
Merged

feat: performance attribution & benchmark-relative return reporting#331
robertocarlous merged 2 commits into
Neurowealth:mainfrom
pope-h:feat/issue-320-performance-attribution

Conversation

@pope-h

@pope-h pope-h commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #320: a Brinson-style, benchmark-relative decomposition of a portfolio's (and a published strategy's) return into allocation and selection effects, precomputed and persisted like the rest of the analytics stack.

  • src/analytics/attribution.ts — pure, zero-I/O core: equal-weighted v1 benchmark (accepts an exogenous series so a real index can be swapped in later), Brinson allocation/selection with the interaction term folded into selection (documented, tested identity), Cariño logarithmic linking across daily periods, and null/unattributed degenerate handling (never Infinity/NaN). Reuses buildDailyRateSeries for the benchmark's forward-fill and follows the same portfolio-value-series convention as strategyMetrics.ts, verified by an anti-divergence test.
  • src/jobs/attribution.ts + PortfolioAttribution/StrategyAttribution Prisma models — one bulk query per run (not per subject), scheduled job wired into src/index.ts with graceful shutdown, migration + hand-written rollback.sql.
  • scripts/backfill-attribution.ts — on-demand recompute for deploy/backfill.
  • GET /api/v1/analytics/attribution?window=30d|90d — owner-scoped, reads the persisted row; { computed: false } (still 200) when nothing has been computed yet.
  • Strategy marketplace entries gain vsBenchmark, merged from StrategyAttribution onto the existing PublishedStrategyMetric page without disturbing the SQL sort/DoS-prevention design.
  • docs/PERFORMANCE_ATTRIBUTION.md (new), docs/openapi.yaml, docs/STRATEGY_MARKETPLACE.md updated.

Test plan

  • tests/unit/analytics/attribution.test.ts — 20 fixture tests covering the Brinson identity, Cariño linking/reconciliation, degenerate cases (wipeouts, empty windows, missing benchmark data), and an anti-divergence check against strategyMetrics.bucketByInstant
  • tests/unit/strategy/service.test.tsvsBenchmark merge behavior in getMarketplace
  • tests/integration/strategies.integration.test.ts — updated mock for the new StrategyAttribution query
  • Full suite green: npm test (924/924), npm run lint, npm run format:check, npx tsc --noEmit
  • Migration + rollback.sql applied and rolled back against a real local Postgres instance
  • End-to-end smoke test of the job against real Postgres (seeded user/position/snapshots/rates) confirming persisted rows reconcile (reconciliationGap ~1e-17, reconciled: true) and correctly report a never-held benchmark sector's pure allocation effect
  • docs/openapi.yaml validated with redocly lint/bundle (same warning count as main, all pre-existing)

pope-h added 2 commits August 19, 2026 16:44
Adds Brinson-style, benchmark-relative decomposition of a portfolio's (and a
published strategy's) return into allocation and selection effects, linked
across a 30/90-day window with Cariño smoothing.

- src/analytics/attribution.ts: pure, zero-I/O Brinson attribution core
  (equal-weighted v1 benchmark, interaction folded into selection, Cariño
  logarithmic linking, null/unattributed degenerate handling — never
  Infinity/NaN), reusing buildDailyRateSeries's forward-fill policy for the
  benchmark side and the strategyMetrics value-series convention for the
  portfolio side.
- src/jobs/attribution.ts + prisma models PortfolioAttribution /
  StrategyAttribution: precomputed, persisted rows (one query per run, not
  per subject), scheduled job wired into src/index.ts with graceful shutdown.
- scripts/backfill-attribution.ts: on-demand recompute for deploy/backfill.
- GET /api/v1/analytics/attribution: owner-scoped, reads the persisted row.
- Strategy marketplace entries gain `vsBenchmark`, merged from
  StrategyAttribution without disturbing the existing SQL sort.
- docs/PERFORMANCE_ATTRIBUTION.md, docs/openapi.yaml, and
  docs/STRATEGY_MARKETPLACE.md updated; anti-divergence test asserts
  attribution's value series agrees with strategyMetrics.bucketByInstant.

Closes Neurowealth#320
getMarketplace now queries StrategyAttribution for vsBenchmark; the
integration test's mocked db needed the same addition already made to the
unit test's mock, or every marketplace request threw on the undefined model.
@robertocarlous
robertocarlous merged commit ff98678 into Neurowealth:main Aug 19, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Performance Attribution & Benchmark-Relative Return Reporting

2 participants